#!/usr/bin/env python
#
# Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
#

# A script to kill hanging process. The tool will return non-zero if any
# process was actually found.
#

import optparse
import os
import signal
import subprocess
import sys

import utils


os_name = utils.GuessOS()

POSIX_INFO = 'ps -p %s -o args'

EXECUTABLE_NAMES = {
  'win32': {
    'chrome': 'chrome.exe',
    'dart': 'dart.exe',
    'dart_precompiled_runtime': 'dart_precompiled_runtime.exe',
    'firefox': 'firefox.exe',
    'gen_snapshot': 'gen_snapshot.exe',
    'git': 'git.exe',
    'iexplore': 'iexplore.exe',
    'vctip': 'vctip.exe',
    'mspdbsrv': 'mspdbsrv.exe',
  },
  'linux': {
    'chrome': 'chrome',
    'dart': 'dart',
    'dart_precompiled_runtime': 'dart_precompiled_runtime',
    'firefox': 'firefox',
    'gen_snapshot': 'gen_snapshot',
    'flutter_tester': 'flutter_tester',
    'git': 'git',
  },
  'macos': {
    'chrome': 'Chrome',
    'chrome_helper': 'Chrome Helper',
    'dart': 'dart',
    'dart_precompiled_runtime': 'dart_precompiled_runtime',
    'firefox': 'firefox',
    'gen_snapshot': 'gen_snapshot',
    'git': 'git',
    'safari': 'Safari',
  }
}

INFO_COMMAND = {
  'win32': 'wmic process where Processid=%s get CommandLine',
  'macos': POSIX_INFO,
  'linux': POSIX_INFO,
}

STACK_INFO_COMMAND = {
  'win32': None,
  'macos': '/usr/bin/sample %s 1 4000 -mayDie',
  'linux': '/usr/bin/eu-stack -p %s',
}

def GetOptions():
  parser = optparse.OptionParser('usage: %prog [options]')
  true_or_false = ['True', 'False']
  parser.add_option("--kill_dart", default='True', type='choice',
                    choices=true_or_false,
                    help="Kill all dart processes")
  parser.add_option("--kill_vc", default='True', type='choice',
                    choices=true_or_false,
                    help="Kill all git processes")
  parser.add_option("--kill_vsbuild", default='False', type='choice',
                    choices=true_or_false,
                    help="Kill all visual studio build related processes")
  parser.add_option("--kill_browsers", default='False', type='choice',
                    choices=true_or_false,
                    help="Kill all browser processes")
  (options, args) = parser.parse_args()
  return options


def GetPidsPosix(process_name):
  # This is to have only one posix command, on linux we could just do:
  # pidof process_name
  cmd = 'ps -e -o pid= -o comm='
  # Sample output:
  # 1 /sbin/launchd
  # 80943 /Applications/Safari.app/Contents/MacOS/Safari
  p = subprocess.Popen(cmd,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       shell=True)
  output, stderr = p.communicate()
  results = []
  lines = output.splitlines()
  for line in lines:
    split = line.split()
    # On mac this ps commands actually gives us the full path to non
    # system binaries.
    if len(split) >= 2 and " ".join(split[1:]).endswith(process_name):
      results.append(split[0])
  return results


def GetPidsWindows(process_name):
  cmd = 'tasklist /FI "IMAGENAME eq %s" /NH' % process_name
  # Sample output:
  # dart.exe    4356 Console            1      6,800 K
  p = subprocess.Popen(cmd,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       shell=True)
  output, stderr = p.communicate()
  results = []
  lines = output.splitlines()

  for line in lines:
    split = line.split()
    if len(split) > 2 and split[0] == process_name:
      results.append(split[1])
  return results

def GetPids(process_name):
  if os_name == "win32":
    return GetPidsWindows(process_name)
  else:
    return GetPidsPosix(process_name)

def PrintPidStackInfo(pid):
  command_pattern = STACK_INFO_COMMAND.get(os_name, False)
  if command_pattern:
    p = subprocess.Popen(command_pattern % pid,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         shell=True)
    stdout, stderr = p.communicate()
    stdout = stdout.splitlines()
    stderr = stderr.splitlines()

    print "  Stack:"
    for line in stdout:
      print "    %s" % line
    if stderr:
      print "  Stack (stderr):"
      for line in stderr:
        print "    %s" % line

def PrintPidInfo(pid, dump_stacks):
  # We assume that the list command will return lines in the format:
  # EXECUTABLE_PATH ARGS
  # There may be blank strings in the output
  p = subprocess.Popen(INFO_COMMAND[os_name] % pid,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       shell=True)
  output, stderr = p.communicate()
  lines = output.splitlines()

  # Pop the header
  lines.pop(0)

  print "Hanging process info:"
  print "  PID: %s" % pid
  for line in lines:
    # wmic will output a bunch of empty strings, we ignore these
    if line: print "  Command line: %s" % line

  if dump_stacks:
    PrintPidStackInfo(pid)

def KillPosix(pid):
  try:
    os.kill(int(pid), signal.SIGKILL)
  except:
    # Ignore this, the process is already dead from killing another process.
    pass

def KillWindows(pid):
  # os.kill is not available until python 2.7
  cmd = "taskkill /F /PID %s" % pid
  p = subprocess.Popen(cmd,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       shell=True)
  p.communicate()

def Kill(name, dump_stacks=False):
  if name not in EXECUTABLE_NAMES[os_name]:
    return 0
  print("***************** Killing %s *****************" % name)
  platform_name = EXECUTABLE_NAMES[os_name][name]
  pids = GetPids(platform_name)
  for pid in pids:
    PrintPidInfo(pid, dump_stacks)
    if os_name == "win32":
      KillWindows(pid)
    else:
      KillPosix(pid)
    print("Killed pid: %s" % pid)
  if len(pids) == 0:
    print("  No %s processes found." % name)
  return len(pids)

def KillBrowsers():
  status = Kill('firefox')
  # We don't give error on killing chrome. It happens quite often that the
  # browser controller fails in killing chrome, so we silently do it here.
  Kill('chrome')
  status += Kill('chrome_helper')
  status += Kill('iexplore')
  status += Kill('safari')
  return status

def KillVCSystems():
  status = Kill('git')
  return status

def KillVSBuild():
  status = Kill('vctip')
  status += Kill('mspdbsrv')
  return status

def KillDart():
  status = Kill("dart", dump_stacks=True)
  status += Kill("gen_snapshot", dump_stacks=True)
  status += Kill("dart_precompiled_runtime", dump_stacks=True)
  status += Kill("flutter_tester", dump_stacks=True)
  return status

def Main():
  options = GetOptions()
  status = 0
  if options.kill_dart == 'True':
    if os_name == "win32":
      # TODO(24086): Add result of KillDart into status once pub hang is fixed.
      KillDart()
    else:
      status += KillDart()
  if options.kill_vc == 'True':
    status += KillVCSystems()
  if options.kill_vsbuild == 'True' and os_name == 'win32':
    status += KillVSBuild()
  if options.kill_browsers == 'True':
    status += KillBrowsers()
  return status

if __name__ == '__main__':
  sys.exit(Main())
